#include "Partido.h"

#include <map>
#include <algorithm>
#include <string>
#include <list>
#include <utility>
#include "Log.h"

Partido::Partido() {
    this->id = ID_INVALIDO;
    this->descripcion = "";
    this->nroNivel = NIVEL_INVALIDO;
    this->estado = PARTIDO_NO_INICIADO;
    this->jugadores_necesarios = CANT_JUG_PREDETERMINADA;
}

Partido::~Partido() {
    for (int i = 0; i < (int)jugadores.size(); i++) {
        jugadores[i]->quitarDePartido();
    }
}

void Partido::finalizar() {
    estado = PARTIDO_FINALIZADO;
}

int Partido::procesar(EventoPartido *evento) {
    if (this->ejecutando()) {
        m_eventos.lock();
        this->eventos.push_back(evento);
        m_eventos.unlock();
    } else {
        delete evento;
        return 1;
    }
    return 0;
}

bool Partido::terminado() {
    return this->estado == PARTIDO_FINALIZADO;
}

bool Partido::aceptando() {
    return this->estado == PARTIDO_NO_INICIADO;
}

void Partido::desbloquear() {
    mutex_general.lock();
}

void Partido::bloquear() {
    mutex_general.unlock();
}

void *Partido::run() {
    Log::loguear(MDEBUG, "Iniciando hilo partido...");
    while (!terminado()) {
        switch (this->estado) {
        case PARTIDO_INICIANDO: {
            this->inicializar();
            this->notificarEscenarioInicial();
            this->estado = PARTIDO_EJECUTANDO;
        }
        break;
        case PARTIDO_EJECUTANDO: {
            procesarEventos();
        }
        break;
        case PARTIDO_CANCELADO: {
            this->notificarFinPartido(FIN_CANCELACION, NULL);
            this->finalizar();
        }
        break;
        default:
            break;
        }
        usleep(TIMING_PARTIDO);
    }
    Log::loguear(MDEBUG, "Terminando partido (exit)...");

    eliminarEventos(-1);

    this->exit();
    return (void *)NULL;
}

void Partido::eliminar() {
    this->estado = PARTIDO_ELIMINADO;
}

void Partido::quitarJugador(int uid) {
    m_jugadores.lock();
    int pos = -1;
    for (int i = 0; i < (int)this->jugadores.size(); i++) {
        if (this->jugadores[i]->getUID() == uid)
            pos = i;
    }
    if (pos != -1) {
        this->jugadores.erase(jugadores.begin() + pos);
        if (this->ejecutando() && jugadores.size() < JUGADORES_MINIMOS)
            estado = PARTIDO_CANCELADO;
    }
    m_jugadores.unlock();
}

void Partido::notificarEscenarioInicial() {
    Mensaje mensaje;
    Json::Value escenario;
    mensaje.data["not_type"] = NOT_TYPE_START_GAME;
    this->getEscenarioInicial(escenario);
    mensaje.data["contenido"] = escenario;
    this->enviarAJugadores(mensaje);
}

void Partido::getEscenarioInicial(Json::Value &escenario) {
    Json::Value estado_jugadores, tablero;

    escenario.clear();
    escenario["idPartido"] = this->getId();
    escenario["nivel"]["numero"] = this->getNumeroNivel();
    escenario["nivel"]["numCols"] =
        this->juego.getNivel()->getCantColumnas();
    escenario["nivel"]["numFilas"] = this->juego.getNivel()->getCantFilas();
    escenario["nivel"]["puntos"] =
        this->juego.getNivel()->getPuntajeRequerido();
    escenario["nivel"]["nombre"] = this->juego.getNivel()->getNombreNivel();
    escenario["nivel"]["imagenFondo"] =
        this->juego.getNivel()->getImagenFondo();
    escenario["nivel"]["sonidoMovimiento"] =
        this->juego.getNivel()->getSonidoMovimiento();
    escenario["nivel"]["sonidoCombinacion"] =
        this->juego.getNivel()->getSonidoCombinacion();

    this->getEstadoJugadores(escenario);

    this->juego.getNivel()->obtenerJSonTablero(tablero);
    escenario["tablero"]=tablero;
}

/** Descripcion: devuelve en "resumen" los datos basicos del partido */
void Partido::getResumenJson(Json::Value &resumen) {
    Json::Value jugador;
    resumen["idPartido"] = this->getId();
    resumen["nivel"] = this->getNumeroNivel();
    resumen["jugNecesarios"] = this->getJugNecesarios();
    std::string jugadores;
    /* Lista separada por comas con los nombres de jugadores */
    for (int i = 0; i < (int)this->jugadores.size(); i++) {
        jugadores.append(this->jugadores[i]->getNombre());
        jugadores.append(", ");
    }
    jugadores.erase(jugadores.size() - 2, 2);
    resumen["jugadores"] = jugadores;
}

int Partido::inicializar() {
    // Inicializa el tablero a partir del json creado con el editor.
    this->juego.iniciar(this->nroNivel);
    return 0;
}

void Partido::imprimirTablero() {
    this->juego.getNivel()->imprimirTablero();
}

int Partido::procesarEventos() {
    m_eventos.lock();
    EventoPartido *evento;
    for (int i = 0; i < (int)this->eventos.size(); i++) {
        evento = eventos[i];
        switch (evento->getTipo()) {
        case TIPO_COM_MOVER: {
            Log::loguear(MDEBUG, "Procesando movimiento...");
            procesarEventoMovimiento(evento);
        }
        break;
        case TIPO_COM_DESCONECTAR_JUGADOR:
            Log::loguear(MDEBUG, "Desconectando jugador...");
            /* Este comando será similar al REQ_OUT_USER solo que se envia por este camino
            una vez que inicio el partido */
            this->quitarJugador(evento->getJugador()->getUID());
            break;
        case TIPO_COM_FINALIZAR:
            Log::loguear(MDEBUG, "Finalizando partido por comando...");
            this->finalizar();
        default:    // Hacer nada para comandos incorrectos.
            break;
        }
    }
    this->eliminarEventos(-1);
    m_eventos.unlock();
    return 0;
}

/** Descripcion: se eliminan de la cola todos los eventos del tipo pasado por
    parametro (pasando -1 se eliminan todos).
    IMPORTANTE: debe llamarse desde un contexto protegido de la lista de eventos
    (p.e. procesarEventos o metodos llamados por este) o mientras no se esté
    ejecutando (destructor). No se protege con mutex. */
void Partido::eliminarEventos(int tipo_comando) {
    if (tipo_comando == -1) {
        for (int i = 0; i < (int) this->eventos.size(); i++)
            delete eventos[i];
        eventos.clear();
    } else {
        for (int i = 0; i < (int) this->eventos.size(); i++)
            if (eventos[i]->getTipo() == tipo_comando) {
                eventos.erase(eventos.begin() + i);
                delete eventos[i];
            }
    }
}

void Partido::procesarEventoMovimiento(EventoPartido *evento) {
    Mensaje mensaje_server;
    int fila, columna;
    bool quedan_movimientos;

    std::pair<int,int> origen(evento->argumentos["x1"].asInt(),
                              evento->argumentos["y1"].asInt());
    std::pair<int,int> destino(evento->argumentos["x2"].asInt(),
                               evento->argumentos["y2"].asInt());

    mensaje_server.data["idPartido"] = this->getId();
    int puntos = 0;
    int error = 0;
    /* Si el movimiento es correcto procesar mensaje a enviarl */
    if ((error = this->mover(origen,destino, mensaje_server.data, puntos)) == 0) {
        this->puntajes[evento->getJugador()->getUID()] += puntos;
        Log::loguear(MDEBUG, "Verificando disponibilidad de movimientos...\n");
        quedan_movimientos=
			this->juego.getNivel()->quedanMovimientos(fila, columna);
//         quedan_movimientos=true;
//         fila=0; columna=0;

        mensaje_server.data["not_type"] = NOT_UPDATE_BOARD;
        /* Movimiento enviado por cliente */
        mensaje_server.data["movimiento"]["x1"] = origen.first;
        mensaje_server.data["movimiento"]["y1"] = origen.second;
        mensaje_server.data["movimiento"]["x2"] = destino.first;
        mensaje_server.data["movimiento"]["y2"] = destino.second;
        /* Queda al menos un movimiento en esta posicion */
        mensaje_server.data["movimiento_posible"]["fila"] = fila;
        mensaje_server.data["movimiento_posible"]["columna"] = columna;
        /* Si fue un movimiento valido se eliminan todos los eventos de movimientos
             que se encuentran actualmente en cola (para evitar conflictos sobre el tablero */
        Log::loguear(MDEBUG, "Eliminando eventos de movimiento.\n");
        this->eliminarEventos(TIPO_COM_MOVER);
        this->getEstadoJugadores(mensaje_server.data);
        /*  Se envia a todos los jugadores el mensaje NOT_UPDATE_BOARD */
        Log::loguear(MDEBUG, "Enviando actualizacion de tablero!\n");
        this->enviarAJugadores(mensaje_server);

        /* Verifico si ganó */
        if (puntajes[evento->getJugador()->getUID()] >= getPuntosNecesarios()) {
            Log::loguear(MDEBUG, "Se han alcanzado los puntos necesarios, hay ganador!\n");
            evento->getJugador()->aumentarNivel();
            this->notificarFinPartido(FIN_GANADOR, evento);
            this->finalizar();
            /* Si no quedan movimientos y no ganó finaliza el partido */
        } else if (!quedan_movimientos) {
            Log::loguear(MDEBUG, "No quedan movimientos!\n");
            this->notificarFinPartido(FIN_SIN_MOVIMIENTOS, NULL);
            this->finalizar();
        }
    } else {
        Log::loguear(MDEBUG, "No se registraron puntos.");
        /* Le notifico el mal movimiento en caso de tener que reacomodar */
        if (error == 1)
            this->notificarMovimientoInvalido(evento);
    }
}

/** Descripcion: se notifica a los jugadores aún conectados que el partido ha finalizado.
    mediante un mensaje tipo NOT_FIN_PARTIDO.
    Parametros: tipo_finalizacion: (FIN_CANCELACION, FIN_GANADOR o FIN_SIN_MOVIMIENTOS)
                evento: opcional, disparador de fin o NULL
*/
void Partido::notificarFinPartido(std::string tipo_finalizacion,
                                  EventoPartido *evento) {
    Mensaje mensaje_server;
    mensaje_server.data["not_type"] = NOT_FIN_PARTIDO;
    mensaje_server.data["contenido"]["causa"] = tipo_finalizacion;
    if (tipo_finalizacion == FIN_GANADOR) {
        mensaje_server.data["contenido"]["ganador"] =
            evento->getJugador()->getNombre();
        mensaje_server.data["contenido"]["puntos"] =
            this->puntajes[evento->getJugador()->getUID()];
    }
    this->enviarAJugadores(mensaje_server);
}

void Partido::notificarMovimientoInvalido(EventoPartido *evento) {
    Mensaje mensaje_server;
    mensaje_server.data["not_type"] = NOT_TYPE_INVALID_MOV;
    mensaje_server.data["contenido"] = evento->argumentos;
    evento->getJugador()->enviar(mensaje_server);
}
/* FIN AGREGADO */
void Partido::getEstadoJugadores(Json::Value &estado_jugadores) {
    Json::Value estado_jugador;
    for (int i = 0; i < (int)this->jugadores.size(); i++) {
        estado_jugador["nombre"] = this->jugadores[i]->getNombre();
        estado_jugador["puntos"] = this->getPuntos(
                                       this->jugadores[i]->getUID());
        estado_jugadores["jugadores"].append(estado_jugador);
    }
}



/** Descripcion: mueve las piezas de la posicion c1 a la c2, generando un
      nuevo estado del tablero.
      Retorno:
      0: exito, el tablero ha cambiado de estado.
      1: movimiento no valido, hubo colision. Se ha intentado mover una ficha
           recientemente modificada.
      2: movimiento no valido (no aplica ninguna regla)
      */
int Partido::mover(std::pair<int,int> origen, std::pair<int,int> destino,
                    Json::Value &cambios_tablero, int &puntaje) {
    m_tablero.lock();
    puntaje = 0;
    int error = 0;

    if (this->juego.getNivel()->movimientoValido(origen, destino)) {
        this->juego.moverFicha(origen, destino, puntaje, cambios_tablero);
      if (puntaje == 0)
          error = 1; // Movimiento valido pero no genera ninguna combinacion.
    } else {
        Log::loguear(MDEBUG, "Error: cliente envia movimiento invalido!");
        error = 2;
    }
    m_tablero.unlock();
    return error;
}

int Partido::getPuntosNecesarios() {
    return this->juego.getNivel()->getPuntajeRequerido();
}

void Partido::iniciar() {
    Log::loguear(MDEBUG, "Iniciando partido...");
    this->estado = PARTIDO_INICIANDO;
    this->start();
}

int Partido::enviarAJugadores(Mensaje &mensaje) {
    int error = 0;

    for (int i = 0; i < (int)jugadores.size(); i++) {
        if (jugadores[i]->enviar(mensaje) <= 0)
            error = 1;
    }
    return error;
}

bool Partido::ejecutando() {
    return this->estado == PARTIDO_EJECUTANDO;
}

void Partido::addJugador(Usuario *usuario) {
    m_jugadores.lock();
    this->jugadores.push_back(usuario);
    m_jugadores.unlock();
}

bool Partido::lleno() {
    return this->getCantEsperando() == this->getJugNecesarios();
}

Usuario *Partido::getJugadorById(int uid) {
    Usuario *jugador = NULL;
    for (int i = 0; i < (int)this->jugadores.size(); i++) {
        if (jugadores[i]->getUID() == uid) {
            jugador = jugadores[i];
        }
    }
    return jugador;
}

/** Descripcion: suma los puntos pasados por parametro.
    * Retorno: nuevo puntaje del jugador.*/
int Partido::sumarPuntos(int uid, int puntos) {
    std::map<int, int>::iterator it;
    int total = 0;
    if ((it = puntajes.find(uid)) == puntajes.end()) {
        puntajes[uid] = total; // 0 puntos
    } else {
        it->second += puntos;
        total = it->second;
    }
    return total;
}

int Partido::getPuntos(int uid) {
    std::map<int, int>::iterator it;
    int puntos = 0;
    if ((it = puntajes.find(uid)) == puntajes.end())
        puntajes[uid] = puntos;
    else
        puntos = it->second;

    return puntos;
}

/** Getters y Setters */
void Partido::setId(int id) {
    this->id = id;
}

int Partido::getId() {
    return this->id;
}

void Partido::setDescripcion(std::string &descripcion) {
    this->descripcion = descripcion;
}
std::string Partido::getDescripcion() {
    return this->descripcion;
}
void Partido::setJugNecesarios(int cant) {
    this->jugadores_necesarios = cant;
}
int Partido::getJugNecesarios() {
    return jugadores_necesarios;
}

int Partido::getNumeroNivel() {
    return nroNivel;
}

void Partido::setNumeroNivel(int nroNivel) {
    this->nroNivel = nroNivel;
}

